import { Callout, Tabs } from 'nextra/components';
import { Widget } from '@/components/demo/widget.tsx';
import LinkBadge from '@/components/ui/link-badge/link-badge.tsx';
import LinkBadgeGroup from '@/components/ui/link-badge/link-badge-group.tsx';

# Persistent Sheet

A persistent sheet is displayed above another widget while still allowing users to interact with the widget below.
It is part of [FScaffold](/docs/layout/scaffold), which should be preferred in most cases.

<LinkBadgeGroup>
    <LinkBadge label="API Reference" href="https://pub.dev/documentation/forui/latest/forui.widgets.sheet/"/>
</LinkBadgeGroup>

<Callout type="info">
    A closely related widget is a [modal sheet](/docs/overlay/sheet) which prevents the user from interacting with the
    rest of the app.
</Callout>

<Callout type="warning">
    All calls to `showFPersistentSheet(...)` should be made inside widgets that have either `FScaffold` or `FSheets` as
    their ancestor.
</Callout>

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='persistent-sheet' query={{}} height={500}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart copy
    class Sheets extends StatefulWidget {
      @override
      State<Sheets> createState() => _State();
    }

    class _State extends State<Sheets> {
      final Map<FLayout, FPersistentSheetController> _controllers = {};

      @override
      Widget build(BuildContext context) {
        VoidCallback onPress(FLayout side) => () {
          for (final MapEntry(:key, :value) in _controllers.entries) {
            if (key != side && value.status.isCompleted) {
              return;
            }
          }

          var controller = _controllers[side];
          if (controller == null) {
            controller = _controllers[side] ??= showFPersistentSheet(
              context: context,
              side: FLayout.ltr,
              builder: (context, controller) => Form(side: side),
            );
          } else {
            controller.toggle();
          }
        };

        return FScaffold( // This can be replaced with FSheets
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            mainAxisSize: MainAxisSize.min,
            spacing: 5,
            children: [
              FButton(
                onPress: onPress(FLayout.ltr),
                child: const Text('Left'),
              ),
              FButton(
                onPress: onPress(FLayout.ttb),
                child: const Text('Top'),
              ),
              FButton(
                onPress: onPress(FLayout.rtl),
                child: const Text('Right'),
              ),
              FButton(
                onPress: onPress(FLayout.btt),
                child: const Text('Bottom'),
              ),
            ],
          ),
        );
      }

      @override
      void dispose() {
        for (final controller in _controllers.values) {
          controller.dispose();
        }
        super.dispose();
      }
    }

    class Form extends StatelessWidget {
      final FLayout side;

      const Form({required this.side, super.key});

      @override
      Widget build(BuildContext context) => Container(
        height: double.infinity,
        width: double.infinity,
        decoration: BoxDecoration(
          color: context.theme.colors.background,
          border: side.vertical
              ? Border.symmetric(horizontal: BorderSide(color: context.theme.colors.border))
              : Border.symmetric(vertical: BorderSide(color: context.theme.colors.border)),
        ),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8.0),
          child: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Account',
                  style: context.theme.typography.xl2.copyWith(
                    fontWeight: FontWeight.w600,
                    color: context.theme.colors.foreground,
                    height: 1.5,
                  ),
                ),
                Text(
                  'Make changes to your account here. Click save when you are done.',
                  style: context.theme.typography.sm.copyWith(
                    color: context.theme.colors.mutedForeground,
                  ),
                ),
                const SizedBox(height: 8),
                SizedBox(
                  width: 450,
                  child: Column(
                    children: [
                      const FTextField(
                        label: Text('Name'),
                        hint: 'John Renalo',
                      ),
                      const SizedBox(height: 10),
                      const FTextField(
                        label: Text('Email'),
                        hint: 'john@doe.com',
                      ),
                      const SizedBox(height: 16),
                      FButton(
                        onPress: () => Navigator.of(context).pop(),
                        child: const Text('Save'),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    }
    ```

  </Tabs.Tab>
</Tabs>

## CLI

To generate and customize this style:

```shell copy
dart run forui style create persistent-sheet
```

## Usage

### `showFPersistentSheet(...)`

```dart copy
showFPersistentSheet(
  context: context,
  style: FPersistentSheetStyle(...),
  side: FLayout.ltr,
  useSafeArea: false,
  keepAliveOffstage: true,
  mainAxisMaxRatio: null,
  constraints: const BoxConstraints(maxWidth: 450, maxHeight: 450),
  draggable: true,
  builder: (context, controller) => const Placeholder(),
);
```

## Examples

### With `KeepAliveOffstage`

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='persistent-sheet' query={{keepAliveOffstage: true}} height={500}/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart {23} copy
    class Sheets extends StatefulWidget {
      @override
      State<Sheets> createState() => _State();
    }

    class _State extends State<Sheets> {
      final Map<FLayout, FPersistentSheetController> _controllers = {};

      @override
      Widget build(BuildContext context) {
        VoidCallback onPress(FLayout side) => () {
          for (final MapEntry(:key, :value) in _controllers.entries) {
            if (key != side && value.status.isCompleted) {
              return;
            }
          }

          var controller = _controllers[side];
          if (controller == null) {
            controller = _controllers[side] ??= showFPersistentSheet(
              context: context,
              side: FLayout.ltr,
              keepAliveOffstage: true,
              builder: (context, controller) => Form(side: side),
            );
          } else {
            controller.toggle();
          }
        };

        return FScaffold( // This can be replaced with FSheets
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            mainAxisSize: MainAxisSize.min,
            spacing: 5,
            children: [
              FButton(
                onPress: onPress(FLayout.ltr),
                child: const Text('Left'),
              ),
              FButton(
                onPress: onPress(FLayout.ttb),
                child: const Text('Top'),
              ),
              FButton(
                onPress: onPress(FLayout.rtl),
                child: const Text('Right'),
              ),
              FButton(
                onPress: onPress(FLayout.btt),
                child: const Text('Bottom'),
              ),
            ],
          ),
        );
      }

      @override
      void dispose() {
        for (final controller in _controllers.values) {
          controller.dispose();
        }
        super.dispose();
      }
    }

    class Form extends StatelessWidget {
      final FLayout side;

      const Form({required this.side, super.key});

      @override
      Widget build(BuildContext context) => Container(
        height: double.infinity,
        width: double.infinity,
        decoration: BoxDecoration(
          color: context.theme.colors.background,
          border: side.vertical
              ? Border.symmetric(horizontal: BorderSide(color: context.theme.colors.border))
              : Border.symmetric(vertical: BorderSide(color: context.theme.colors.border)),
        ),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8.0),
          child: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Account',
                  style: context.theme.typography.xl2.copyWith(
                    fontWeight: FontWeight.w600,
                    color: context.theme.colors.foreground,
                    height: 1.5,
                  ),
                ),
                Text(
                  'Make changes to your account here. Click save when you are done.',
                  style: context.theme.typography.sm.copyWith(
                    color: context.theme.colors.mutedForeground,
                  ),
                ),
                const SizedBox(height: 8),
                SizedBox(
                  width: 450,
                  child: Column(
                    children: [
                      const FTextField(
                        label: Text('Name'),
                        hint: 'John Renalo',
                      ),
                      const SizedBox(height: 10),
                      const FTextField(
                        label: Text('Email'),
                        hint: 'john@doe.com',
                      ),
                      const SizedBox(height: 16),
                      FButton(
                        onPress: () => Navigator.of(context).pop(),
                        child: const Text('Save'),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    }
    ```

  </Tabs.Tab>
</Tabs>

### With `DraggableScrollableSheet`

<Tabs items={['Preview', 'Code']}>
  <Tabs.Tab>
    <Widget name='persistent-sheet' query={{}} height={400} variant='draggable'/>
  </Tabs.Tab>
  <Tabs.Tab>
    ```dart copy
    class DraggableSheets extends StatefulWidget {
      @override
      State<DraggableSheets> createState() => _State();
    }

    class _State extends State<DraggableSheets> {
      FPersistentSheetController? controller;

      @override
      Widget build(BuildContext context) => FScaffold(
        child: FButton(
          child: const Text('Click me'),
          onPress: () {
            if (controller != null) {
              controller!.toggle();
              return;
            }

            controller = showFPersistentSheet(
              context: context,
              side: FLayout.btt,
              mainAxisMaxRatio: null,
              builder: (context, _) => DraggableScrollableSheet(
                expand: false,
                builder: (context, controller) => ScrollConfiguration(
                  // This is required to enable dragging on desktop.
                  // See https://github.com/flutter/flutter/issues/101903 for more information.
                  behavior: ScrollConfiguration.of(context).copyWith(
                    dragDevices: {
                      PointerDeviceKind.touch,
                      PointerDeviceKind.mouse,
                      PointerDeviceKind.trackpad,
                    },
                  ),
                  child: FTileGroup.builder(
                    count: 25,
                    scrollController: controller,
                    tileBuilder: (context, index) => FTile(title: Text('Tile $index')),
                  ),
                ),
              ),
            );
          },
        ),
      );

      @override
      void dispose() {
        controller?.dispose();
        super.dispose();
      }
    }
    ```

  </Tabs.Tab>
</Tabs>
